﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WcfCGServices.Contracts.Messages;
using System.ComponentModel.Composition.Hosting;
using WcfCGServices.Contracts.Data;
using System.IO;
using Raona.Code.Contracts.Inspection;
using Raona.Code.Model;
using WcfClientGenerator.Model.Config;
using WcfClientGenerator.Model.Common;
using Raona.Code.Contracts.Generation;
using System.ServiceModel;
using System.Diagnostics;
using System.ComponentModel.Composition;
using System.Configuration;
using System.Web.Hosting;
using System.Reflection;
using System.Threading.Tasks;
using System.IO.Packaging;

namespace WcfCGServices.Library
{
    public class WCGService : IWCGService, IDisposable
    {
        #region Constants

        // Relative path to the folder containing the templates assembly
        private const string TemplatesFolder = "~/Content/Templates";
        // Relative path to the folder containing the inspector assembly
        private const string InspectorFolder = "~/Content/Inspector";
        // Inspector assembly name
        private const string InspectorAssemblyName = "Raona.Code.AssemblyInspection.v2.dll";

        private const string ReceivedPackageFileName = "_package.zip";
        private const string ProxiesPackageFileName = "_proxies.zip";
        private const string PackageFolderPrefix = "tmp.";
        private const string SyncProxiesProjectName = "SyncProject";
        private const string AsyncProxiesProjectName = "AsyncProject";

        #endregion

        #region Fields

        /// <summary>
        /// The container that resolves the code generation dependencies
        /// </summary>
        private CompositionContainer container = null;

        #endregion

        #region Constructors

        public WCGService()
        {
            this.InitializeContainer();
        }

        #endregion

        #region IWCGService Members

        public GenerationResponse GenerateProxies(GenerationRequest request)
        {
            GenerationResponse response = new GenerationResponse();

            if (this.ValidateRequest(request))
            {
                // Create a unique subfolder under the designated working folder
                string tempPath = Path.Combine(this.TempFolder, string.Concat(WCGService.PackageFolderPrefix, Path.GetRandomFileName()));
                if (Directory.Exists(tempPath))
                    Directory.Delete(tempPath, true);
                Directory.CreateDirectory(tempPath);

                // Choose a name for the file that will contain the received package
                string packagePath = Path.Combine(tempPath, WCGService.ReceivedPackageFileName);

                // Copy the received stream in the package file stream
                using (FileStream packageStream = File.Open(packagePath, FileMode.Create, FileAccess.Write))
                {
                    request.RequestData.CopyTo(packageStream);
                    
                    packageStream.Close();
                }

                // Extract contracts assembly and dependencies
                this.UnpackAssemblies(packagePath, tempPath);

                // Inspect contracts
                string contractsPath = Path.Combine(tempPath, request.Arguments.ContractsAssemblyFileName);
                IOperationContractInspector inspector = this.CreateInspector(contractsPath, "Raona.Code.AssemblyInspection.AssemblyOperationContractInspector2", WCGService.InspectorAssemblyName);
                //IOperationContractInspector inspector = this.CreateInspector(contractsPath);
                Dictionary<string, Namespace> namespaces = inspector.GetInterfaces(
                    request.Arguments.ClientBaseName,
                    request.Arguments.ServiceContractsNamespace);
                this.UnloadInspector(ref inspector);

                // Configure generator
                this.TemplateGenerator.Namespaces = namespaces.Values.AsEnumerable();
                WcfClientConfiguration configuration = new WcfClientConfiguration
                {
                    AsyncContractsFileName = request.Arguments.AsyncContractsFileName,
                    AsyncProxiesFileName = request.Arguments.AsyncProxiesFileName,
                    SyncContractsFileName = request.Arguments.SyncContractsFileName,
                    SyncProxiesFileName = request.Arguments.SyncProxiesFileName,
                    ServiceContractsNamespace = request.Arguments.ServiceContractsNamespace,
                    ClientBaseName = request.Arguments.ClientBaseName,
                    SpecificUsings = request.Arguments.SpecificUsings,
                    GenerationFolder = tempPath,
                    AsyncDestinationProjects = request.Arguments.AsyncProxiesNeeded ?
                        new List<string>(new string[] { WCGService.AsyncProxiesProjectName })
                        : new List<string>(),
                    SyncDestinationProjects = request.Arguments.SyncProxiesNeeded ?
                        new List<string>(new string[] { WCGService.SyncProxiesProjectName })
                        : new List<string>(),
                };
                this.TemplateGenerator.Configuration = SerializationManager.XmlSerializeString(configuration);

                // Generate proxy files
                Dictionary<string, List<GeneratedFile>> generatedFiles = this.TemplateGenerator.Generate();
                //foreach (string fileName in Directory.EnumerateFiles(Path.Combine(this.TempFolder, "src"), "*.cs"))
                //{
                //    File.Copy(fileName, Path.Combine(tempPath, Path.GetFileName(fileName)));
                //}

                // Create response package
                string proxiesPackagePath = Path.Combine(tempPath, WCGService.ProxiesPackageFileName);
                this.PackResult(tempPath, proxiesPackagePath);
                FileStream proxiesPackageStream = File.OpenRead(proxiesPackagePath);
                response.ResponseData = proxiesPackageStream;

                // Clean up after operation completes
                OperationContext.Current.OperationCompleted += (sender, e) =>
                {
                    // Close respose stream
                    if (proxiesPackageStream != null)
                        proxiesPackageStream.Close();

                    // Delete temporary files
                    if (Directory.Exists(tempPath))
                    {
                        try
                        {
                            Directory.Delete(tempPath, true);
                        }
                        catch (Exception ex)
                        {
                            Debug.WriteLine(ex.StackTrace);
                        }
                    }
                };
            }

            return response;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (this.container != null)
            {
                this.container.Dispose();
                this.container = null;
            }
        }

        #endregion

        #region Properties (Public)

        [Import]
        public ITemplateGenerator TemplateGenerator { get; set; }

        #endregion

        #region Properties (Private)

        private string TempFolder
        {
            get
            {
                return ConfigurationManager.AppSettings["TempFolder"];
            }
        }

        private string TemplatesFolderPath
        {
            get
            {
                return WCGService.GetAbsoluteSubfolder(WCGService.TemplatesFolder);
            }
        }

        private string InspectorFolderPath
        {
            get
            {
                return WCGService.GetAbsoluteSubfolder(WCGService.InspectorFolder);
            }
        }

        #endregion

        #region Methods


        private static string GetAbsoluteSubfolder(string relativeSubfolder)
        {
            string templatesFolder = null;
            if (HostingEnvironment.IsHosted)
                templatesFolder = HostingEnvironment.MapPath(relativeSubfolder);
            else
            {
                string baseFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                string templatesPath = relativeSubfolder.Substring(2).Replace('/', '\\');
                templatesFolder = Path.Combine(baseFolder, templatesPath);
            }
            return templatesFolder;
        }

        #region Create inspector in AppDomain

        private AppDomain contractInspectorAppDomain = null;

        private string ContractsInspectorAppDomainCodeBase
        {
            get
            {
                string location = this.InspectorFolderPath;
                return location;
            }
        }

        private IOperationContractInspector CreateInspector(string contractsAssemblyPath, string inspectorTypeName, string inspectorAssemblyFileName)
        {
            string location = this.ContractsInspectorAppDomainCodeBase;
            string inspectorAssemblyPath = Path.Combine(location, inspectorAssemblyFileName);
            AppDomainSetup setup = new AppDomainSetup
            {
                ApplicationBase = location
            };
            // Create a new app domain
            this.contractInspectorAppDomain = AppDomain.CreateDomain("CodeGenerationDomain", null, setup);
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(this.OnAssemblyResolve);
            AssemblyName assemblyName = AssemblyName.GetAssemblyName(inspectorAssemblyPath);
            // Create the contracts inspector in the new domain so it can be unloaded and
            // therefore the contracts assembly is not locked by the addin
            IOperationContractInspector inspector = (IOperationContractInspector)this.contractInspectorAppDomain
                .CreateInstanceFromAndUnwrap(inspectorAssemblyPath, inspectorTypeName, false, BindingFlags.Default, null, new object[] { contractsAssemblyPath }, null, null);

            return inspector;
        }

        private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
        {
            if (args.Name.Contains("XmlSerializers"))
                return null;

            string codeBase = this.ContractsInspectorAppDomainCodeBase;
            AssemblyName assemblyName = new AssemblyName(args.Name);
            string modulePath = string.Format("{0}\\{1}.dll", codeBase, assemblyName.Name);
            if (File.Exists(modulePath))
                assemblyName = AssemblyName.GetAssemblyName(modulePath);
            return ((AppDomain)sender).Load(assemblyName);
        }

        private void UnloadInspector(ref IOperationContractInspector contractInspector)
        {
            if (contractInspector != null)
            {
                contractInspector.Dispose();
                contractInspector = null;
            }
            try
            {
                Task task = new Task(this.UnloadInspectorAppDomain);
                System.Threading.Timer timer = new System.Threading.Timer(_ =>
                    task.Start(),
                    null,
                    100,
                    System.Threading.Timeout.Infinite);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }

        }

        private void UnloadInspectorAppDomain()
        {
            AppDomain.CurrentDomain.AssemblyResolve -= new ResolveEventHandler(this.OnAssemblyResolve);
            if (this.contractInspectorAppDomain != null)
            {
                AppDomain.Unload(this.contractInspectorAppDomain);
                this.contractInspectorAppDomain = null;
            }
        }

        #endregion

        private bool ValidateRequest(GenerationRequest request)
        {
            return request != null
                && request.RequestData != null;
        }

        private void UnpackAssemblies(string packagePath, string destinationFolder)
        {
            using (Package package = Package.Open(packagePath, FileMode.Open, FileAccess.Read))
            {
                foreach (PackagePart part in package.GetParts())
                {
                    string partFileName = part.Uri.ToString().Trim('/');
                    string filePath = Path.Combine(destinationFolder, partFileName);
                    using (Stream partStream = part.GetStream(FileMode.Open, FileAccess.Read))
                    {
                        using (Stream fileStream = File.Open(filePath, FileMode.Create, FileAccess.Write))
                        {
                            partStream.CopyTo(fileStream);

                            fileStream.Close();
                        }
                    }
                }

                package.Close();
            }
        }

        private void PackResult(string folderPath, string destinationPath)
        {
            using (Package package = Package.Open(destinationPath, FileMode.Create))
            {
                DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
                foreach (FileInfo fileInfo in directoryInfo.EnumerateFiles("*.cs"))
                {
                    string fileName = ".\\" + Path.GetFileName(fileInfo.Name);
                    Uri uri = PackUriHelper.CreatePartUri(new Uri(fileName, UriKind.Relative));
                    PackagePart part = package.CreatePart(uri, "", CompressionOption.Normal);
                    using (FileStream fileStream = fileInfo.OpenRead())
                    {
                        using (Stream partStream = part.GetStream())
                        {
                            fileStream.CopyTo(partStream);
                        }
                    }
                }
            }
        }

        private void InitializeContainer()
        {
            string catalogFolder = this.TemplatesFolderPath;
            DirectoryCatalog catalog = new DirectoryCatalog(catalogFolder);
            this.container = new CompositionContainer(catalog);
            CompositionBatch compositionBatch = new CompositionBatch();
            compositionBatch.AddPart(this);
            try
            {
                this.container.Compose(compositionBatch);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.StackTrace);
                this.container.Dispose();
                this.container = null;
            }
        }

        #endregion
    }
}
